/*
  stm32flash - Open Source ST STM32 flash program for *nix
  Copyright (C) 2010 Geoffrey McRae <geoff@spacevs.com>

  This program is free software; you can redistribute it and/or
  modify it under the terms of the GNU General Public License
  as published by the Free Software Foundation; either version 2
  of the License, or (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
*/

#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h>
#include <unistd.h>
#include <sys/ioctl.h>

#include "serial.h"

struct serial
{
    int fd;
    struct termios oldtio;
    struct termios newtio;
    char setup_str[11];
};

static serial_t *serial_open(const char *device)
{
    serial_t *h = calloc(sizeof(serial_t), 1);

    h->fd = open(device, O_RDWR | O_NOCTTY | O_NDELAY);
    if (h->fd < 0)
    {
        free(h);
        return NULL;
    }
    fcntl(h->fd, F_SETFL, 0);

    tcgetattr(h->fd, &h->oldtio);
    tcgetattr(h->fd, &h->newtio);

    return h;
}

static void serial_flush(const serial_t *h)
{
    tcflush(h->fd, TCIOFLUSH);
}

//NOTE: At least on FTDI chip one should wait for some time before calling this function.
// This is beacuse of LOWER level buffering in kernel, as tcflush(h->fd, TCIOFLUSH) does not help.
static void serial_close(serial_t *h)
{
    serial_flush(h);
    tcsetattr(h->fd, TCSANOW, &h->oldtio);
    close(h->fd);
    free(h);
}

static int serial_setup(serial_t *h, const serial_baud_t baud,
                        const serial_bits_t bits,
                        const serial_parity_t parity,
                        const serial_stopbit_t stopbit,
                        const int timeout)
{
    speed_t	port_baud;
    tcflag_t port_bits;
    tcflag_t port_parity;
    tcflag_t port_stop;
    struct termios settings;

    switch (baud)
    {
    case SERIAL_BAUD_1200:
        port_baud = B1200;
        break;
    case SERIAL_BAUD_1800:
        port_baud = B1800;
        break;
    case SERIAL_BAUD_2400:
        port_baud = B2400;
        break;
    case SERIAL_BAUD_4800:
        port_baud = B4800;
        break;
    case SERIAL_BAUD_9600:
        port_baud = B9600;
        break;
    case SERIAL_BAUD_19200:
        port_baud = B19200;
        break;
    case SERIAL_BAUD_38400:
        port_baud = B38400;
        break;
    case SERIAL_BAUD_57600:
        port_baud = B57600;
        break;
    case SERIAL_BAUD_115200:
        port_baud = B115200;
        break;
    case SERIAL_BAUD_230400:
        port_baud = B230400;
        break;
#ifdef B460800
    case SERIAL_BAUD_460800:
        port_baud = B460800;
        break;
#endif /* B460800 */
#ifdef B921600
    case SERIAL_BAUD_921600:
        port_baud = B921600;
        break;
#endif /* B921600 */
#ifdef B500000
    case SERIAL_BAUD_500000:
        port_baud = B500000;
        break;
#endif /* B500000 */
#ifdef B576000
    case SERIAL_BAUD_576000:
        port_baud = B576000;
        break;
#endif /* B576000 */
#ifdef B1000000
    case SERIAL_BAUD_1000000:
        port_baud = B1000000;
        break;
#endif /* B1000000 */
#ifdef B1500000
    case SERIAL_BAUD_1500000:
        port_baud = B1500000;
        break;
#endif /* B1500000 */
#ifdef B2000000
    case SERIAL_BAUD_2000000:
        port_baud = B2000000;
        break;
#endif /* B2000000 */

    case SERIAL_BAUD_INVALID:
    default:
        return PORT_ERR_UNKNOWN;
    }

    switch (bits)
    {
    case SERIAL_BITS_5:
        port_bits = CS5;
        break;
    case SERIAL_BITS_6:
        port_bits = CS6;
        break;
    case SERIAL_BITS_7:
        port_bits = CS7;
        break;
    case SERIAL_BITS_8:
        port_bits = CS8;
        break;

    default:
        return PORT_ERR_UNKNOWN;
    }

    switch (parity)
    {
    case SERIAL_PARITY_NONE:
        port_parity = 0;
        break;
    case SERIAL_PARITY_EVEN:
        port_parity = INPCK | PARENB;
        break;
    case SERIAL_PARITY_ODD:
        port_parity = INPCK | PARENB | PARODD;
        break;

    default:
        return PORT_ERR_UNKNOWN;
    }

    switch (stopbit)
    {
    case SERIAL_STOPBIT_1:
        port_stop = 0;
        break;
    case SERIAL_STOPBIT_2:
        port_stop = CSTOPB;
        break;

    default:
        return PORT_ERR_UNKNOWN;
    }

    /* reset the settings */
#ifndef __sun		/* Used by GNU and BSD. Ignore __SVR4 in test. */
    cfmakeraw(&h->newtio);
#else /* __sun */
    h->newtio.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR
                           | IGNCR | ICRNL | IXON);
    h->newtio.c_oflag &= ~OPOST;
    h->newtio.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
    h->newtio.c_cflag &= ~(CSIZE | PARENB);
    h->newtio.c_cflag |= CS8;
#endif /* __sun */
#ifdef __QNXNTO__
    h->newtio.c_cflag &= ~(CSIZE | IHFLOW | OHFLOW);
#else
    h->newtio.c_cflag &= ~(CSIZE | CRTSCTS);
#endif
    h->newtio.c_cflag &= ~(CSIZE | CRTSCTS);
    h->newtio.c_iflag &= ~(IXON | IXOFF | IXANY | IGNPAR);
    h->newtio.c_lflag &= ~(ECHOK | ECHOCTL | ECHOKE);
    h->newtio.c_oflag &= ~(OPOST | ONLCR);

    /* setup the new settings */
    cfsetispeed(&h->newtio, port_baud);
    cfsetospeed(&h->newtio, port_baud);
    h->newtio.c_cflag |=
        port_parity	|
        port_bits	|
        port_stop	|
        CLOCAL		|
        CREAD;

    h->newtio.c_cc[VMIN] = 0;
    h->newtio.c_cc[VTIME] = timeout;	/* in units of 0.1 s */

    /* set the settings */
    serial_flush(h);
    if (tcsetattr(h->fd, TCSANOW, &h->newtio) != 0)
        return PORT_ERR_UNKNOWN;

    /* confirm they were set */
    tcgetattr(h->fd, &settings);
    if (settings.c_iflag != h->newtio.c_iflag ||
            settings.c_oflag != h->newtio.c_oflag ||
            settings.c_cflag != h->newtio.c_cflag ||
            settings.c_lflag != h->newtio.c_lflag)
        return PORT_ERR_UNKNOWN;

    snprintf(h->setup_str, sizeof(h->setup_str), "%u %d%c%d",
             serial_get_baud_int(baud),
             serial_get_bits_int(bits),
             serial_get_parity_str(parity),
             serial_get_stopbit_int(stopbit));
    return PORT_ERR_OK;
}

int yapy_serial_open( void **port,
                      const char *device,
                      const unsigned int baud,
                      const char * serial_mode,
                      const int timeout )
{
    serial_t *h;

    /* 1. check device name match */
    if (strncmp(device, "/dev/tty", strlen("/dev/tty")))
        return PORT_ERR_NODEV;

    /* 2. check options */
    if (serial_get_baud(baud) == SERIAL_BAUD_INVALID)
        return PORT_ERR_UNKNOWN;
    if (serial_get_bits(serial_mode) == SERIAL_BITS_INVALID)
        return PORT_ERR_UNKNOWN;
    if (serial_get_parity(serial_mode) == SERIAL_PARITY_INVALID)
        return PORT_ERR_UNKNOWN;
    if (serial_get_stopbit(serial_mode) == SERIAL_STOPBIT_INVALID)
        return PORT_ERR_UNKNOWN;

    /* 3. open it */
    h = serial_open(device);
    if (h == NULL)
        return PORT_ERR_UNKNOWN;

    /* 4. set options */
    if (serial_setup(h,
                     serial_get_baud(baud),
                     serial_get_bits(serial_mode),
                     serial_get_parity(serial_mode),
                     serial_get_stopbit(serial_mode),
                     timeout
                    ) != PORT_ERR_OK)
    {
        serial_close(h);
        return PORT_ERR_UNKNOWN;
    }

    *port = h;
    return PORT_ERR_OK;
}

int yapy_serial_close(void **port)
{
    serial_t *h;

    h = (serial_t *)*port;
    if (h == NULL)
        return PORT_ERR_UNKNOWN;

    serial_close(h);
    *port = NULL;
    return PORT_ERR_OK;
}

int yapy_serial_read(void **port, void *buf,
                     size_t nbyte)
{
    serial_t *h;
    ssize_t r;
    uint8_t *pos = (uint8_t *)buf;

    h = (serial_t *)*port;
    if (h == NULL)
        return PORT_ERR_UNKNOWN;

    while (nbyte)
    {
        r = read(h->fd, pos, nbyte);
        if (r == 0)
            return PORT_ERR_TIMEDOUT;
        if (r < 0)
            return PORT_ERR_UNKNOWN;

        nbyte -= r;
        pos += r;
    }
    return PORT_ERR_OK;
}

int yapy_serial_write(void **port, void *buf,
                      size_t nbyte)
{
    serial_t *h;
    ssize_t r;
    const uint8_t *pos = (const uint8_t *)buf;

    h = (serial_t *)*port;
    if (h == NULL)
        return PORT_ERR_UNKNOWN;

    while (nbyte)
    {
        r = write(h->fd, pos, nbyte);
        if (r < 1)
            return PORT_ERR_UNKNOWN;

        nbyte -= r;
        pos += r;
    }
    return PORT_ERR_OK;
}

int yapy_serial_gpio(void **port,
                     int n, int level)
{
    serial_t *h;
    int bit, lines;

    h = (serial_t *)*port;
    if (h == NULL)
        return PORT_ERR_UNKNOWN;

    switch (n)
    {
    case GPIO_RTS:
        bit = TIOCM_RTS;
        break;

    case GPIO_DTR:
        bit = TIOCM_DTR;
        break;

    case GPIO_BRK:
        if (level == 0)
            return PORT_ERR_OK;
        if (tcsendbreak(h->fd, 1))
            return PORT_ERR_UNKNOWN;
        return PORT_ERR_OK;

    default:
        return PORT_ERR_UNKNOWN;
    }

    /* handle RTS/DTR */
    if (ioctl(h->fd, TIOCMGET, &lines))
        return PORT_ERR_UNKNOWN;
    lines = level ? lines | bit : lines & ~bit;
    if (ioctl(h->fd, TIOCMSET, &lines))
        return PORT_ERR_UNKNOWN;

    return PORT_ERR_OK;
}

const char *yapy_serial_get_cfg_str(void **port)
{
    serial_t *h;

    h = (serial_t *)*port;
    return h ? h->setup_str : "INVALID";
}
